/*
 * Copyright (c) 2013, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.omorphdb.core.storage;

import static org.omorphdb.core.storage.StorageConstants.REFERENCED_DOC;
import static org.omorphdb.core.storage.StorageConstants.REMOVED_DOC;
import static org.omorphdb.core.storage.StorageConstants.ROOT_LEVEL_DOC;
import static org.omorphdb.core.value.ValueConstants._ARRAY;
import static org.omorphdb.core.value.ValueConstants._BOOLEAN;
import static org.omorphdb.core.value.ValueConstants._BYTE;
import static org.omorphdb.core.value.ValueConstants._CHAR;
import static org.omorphdb.core.value.ValueConstants._DATE;
import static org.omorphdb.core.value.ValueConstants._DOCUMENT;
import static org.omorphdb.core.value.ValueConstants._DOUBLE;
import static org.omorphdb.core.value.ValueConstants._FLOAT;
import static org.omorphdb.core.value.ValueConstants._INT;
import static org.omorphdb.core.value.ValueConstants._LONG;
import static org.omorphdb.core.value.ValueConstants._SHORT;
import static org.omorphdb.core.value.ValueConstants._STRING;

import java.io.IOException;
import java.util.Date;

import org.omorphdb.core.exception.ConcurrentRequestException;
import org.omorphdb.core.exception.CorruptedStorageException;
import org.omorphdb.core.index.unique.DefaultIndex;
import org.omorphdb.core.io.Appender;
import org.omorphdb.core.io.Reader;
import org.omorphdb.core.io.Resource;
import org.omorphdb.core.io.ResourceFinder;
import org.omorphdb.core.io.Writer;
import org.omorphdb.core.storage.document.Document;
import org.omorphdb.core.storage.document.DocumentDescriptor;
import org.omorphdb.core.storage.document.Field;
import org.omorphdb.core.storage.document.FieldFactory;
import org.omorphdb.core.value.ArrayValue;
import org.omorphdb.core.value.Value;

/**
 * Contains methods to write documents to the storage file.
 * 
 * @author Marco Tamburelli
 */
class DocumentWriter
{
	private final FieldFactory fieldFactory;
	private final ResourceFinder finder;
	private final Resource resource;

	private final DefaultIndex defaultIdx;

	private final Writer writer;
	private final Appender appender;
	private final Reader reader;

	/**
	 * Constructor.
	 * 
	 * @param fieldFactory The specific {@link FieldFactory} implementation;
	 * @param finder The specific {@link ResourceFinder} implementation.
	 * @throws IOException
	 */
	public DocumentWriter(FieldFactory fieldFactory, ResourceFinder finder) throws IOException
	{
		this.fieldFactory = fieldFactory;
		this.finder = finder;

		this.resource = finder.acquireStorage();
		this.defaultIdx = new DefaultIndex(finder.acquireDefaultIndex());

		this.writer = resource.createWriter();
		this.appender = resource.createAppender();
		this.reader = resource.createReader();
	}

	/**
	 * Saves a document. In case the document was never changed, nothing will
	 * happen.
	 * 
	 * A new created document will be inserted at the root level. Any other new
	 * created document appended to this one will be saved but not at the root
	 * level.
	 * 
	 * @param doc
	 * @throws IOException
	 */
	public void save(DocumentImpl doc) throws IOException
	{
		save(doc, true);
	}

	/**
	 * Removes the provided document from the base.
	 * 
	 * @param doc
	 * @throws IOException
	 */
	public void remove(DocumentImpl doc) throws IOException
	{
		remove((DocumentDescriptorImpl) doc.getDescriptor());
	}

	private void save(DocumentImpl doc, boolean rootLevel) throws IOException
	{
		DocumentDescriptorImpl descr = (DocumentDescriptorImpl) doc.getDescriptor();

		if (descr.isUpdated())
		{
			remove(descr);

			this.appender.reserve(StorageConstants.DOC_DESCRIPTOR_SIZE + descr.docSize);

			descr.docPosition = this.appender.getCursor();

			/* The default index should be updated */
			if (descr.status == DocumentStatus.NEW)
			{
				descr.key = this.defaultIdx.registerFirstAvailableKey(descr.docPosition);
			}
			else
			{
				this.defaultIdx.register(descr.key, descr.docPosition);
			}

			writeDescriptor(descr, rootLevel);

			for (Field<?> field : doc.fields())
			{
				this.appender.appendString(field.name());
				writeValue(field.getValue());

				/* Firing the index update */
				field.doAfterSave();
			}
		}
	}

	private void remove(DocumentDescriptorImpl descr) throws IOException
	{
		if (descr.status != DocumentStatus.NEW)
		{
			/*
			 * Checking if the physical document at this expected position has
			 * not been removed by some other process.
			 */
			this.reader.setCursor(descr.docPosition);
			if (this.reader.readByte() == REMOVED_DOC)
				throw new ConcurrentRequestException(
						"The current document has been removed or updated by another process.");

			this.writer.setCursor(descr.docPosition);

			this.writer.writeByte(REMOVED_DOC);
		}
	}

	private void writeDescriptor(DocumentDescriptorImpl descr, boolean rootLevel) throws IOException
	{
		switch (descr.getDocumentStatus())
		{
		case NEW:
			if (rootLevel)
			{
				this.appender.appendByte(ROOT_LEVEL_DOC);
				descr.status = DocumentStatus.ROOT_LEVEL;
			}
			else
			{
				this.appender.appendByte(REFERENCED_DOC);
				descr.status = DocumentStatus.REFERENCED;
			}
			break;

		case REFERENCED:
			this.appender.appendByte(REFERENCED_DOC);
			break;

		case ROOT_LEVEL:
			this.appender.appendByte(ROOT_LEVEL_DOC);
			break;

		default:
			throw new CorruptedStorageException("Invalid status for document: " + descr.getDocumentStatus());
		}

		this.appender.appendLong(descr.key);
		this.appender.appendInt(descr.classID);
		this.appender.appendInt(descr.attributesCount);
		this.appender.appendInt(descr.docSize);
	}

	private void writeValue(Value<?> value) throws IOException
	{
		switch (value.getType())
		{
		case BOOLEAN:
			this.appender.appendType(_BOOLEAN);
			this.appender.appendBoolean((Boolean) value.get());
			break;

		case BYTE:
			this.appender.appendType(_BYTE);
			this.appender.appendByte((Byte) value.get());
			break;

		case CHAR:
			this.appender.appendType(_CHAR);
			this.appender.appendChar((Character) value.get());
			break;

		case SHORT:
			this.appender.appendType(_SHORT);
			this.appender.appendShort((Short) value.get());
			break;

		case INT:
			this.appender.appendType(_INT);
			this.appender.appendInt((Integer) value.get());
			break;

		case LONG:
			this.appender.appendType(_LONG);
			this.appender.appendLong((Long) value.get());
			break;

		case FLOAT:
			this.appender.appendType(_FLOAT);
			this.appender.appendFloat((Float) value.get());
			break;

		case DOUBLE:
			this.appender.appendType(_DOUBLE);
			this.appender.appendDouble((Double) value.get());
			break;

		case STRING:
			this.appender.appendType(_STRING);
			String v = (String) value.get();
			this.appender.appendStringAsBytes(v.getBytes());
			break;

		case DATE:
			this.appender.appendType(_DATE);
			Date d = (Date) value.get();
			this.appender.appendLong(d.getTime());
			break;

		case DOCUMENT:
			this.appender.appendType(_DOCUMENT);
			Document doc = (Document) value.get();
			DocumentDescriptor descr = doc.getDescriptor();

			if (descr.isUpdated())
			{
				DocumentWriter parallelWriter = new DocumentWriter(this.fieldFactory, this.finder);
				parallelWriter.save((DocumentImpl) doc, false);
			}

			this.appender.appendLong(descr.getDefaultKey());
			break;

		case ARRAY:
			ArrayValue av = (ArrayValue) value;
			this.appender.appendType(_ARRAY);

			int l = av.getLength();
			this.appender.appendInt(l);

			for (int i = 0; i < l; i++)
				writeValue(av.get(i));
			break;
		}
	}
}
